/**
 * 
 */
package metagrammar;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkPositionIndex;

import java.io.Serializable;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlID;
import javax.xml.bind.annotation.XmlList;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

import jaxb.metagrammar.OperatorInstanceAdapter;
import jaxb.metagrammar.OperatorLinkedHashMultimapAdapter;
import jaxb.metagrammar.TypeToUUIDStringAdapter;
import jaxb.util.UUIDtoStringAdapter;
import parsetree.OperatorSpec;
import util.HasId;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.LinkedHashMultimap;

/**
 * An operator is a relation
 *      (T1, T2,...,Tn) → T
 * and a set of axiomatic transformations from
 *      (t1, t2,...,tn) ├ U
 * where ti is a syntax tree that results in expressed type Ti,
 * and U is some other syntax tree in the grammar
 * 
 * @author Joshua Lockerman
 * 
 */
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Operator implements Typed, Serializable, HasId {
    
    public static final Operator undefinedOperator = new Operator(Type.UNTYPED);
    public static final Operator.Instance undefinedInstance = undefinedOperator
    .getInstance();
    
    /**
     * @author Joshua Lockerman
     *
     */
    @XmlJavaTypeAdapter(OperatorInstanceAdapter.class)
    public class Instance implements Typed {
        
        private final Operator.Instance[] inputTuple;
        
        protected Instance()
        {
            this.inputTuple = new Instance[inputTypes.length];
            Arrays.fill(inputTuple, undefinedInstance);
        }
        
        private Instance(Operator.Instance[] inputTuple)
        {
            this.inputTuple = new Instance[inputTypes.length];
            for(int i = 0; i < inputTuple.length; i++) {
                this.inputTuple[i] = inputTuple[i];
            }
        }
        
        /* (non-Javadoc)
         * @see metagrammar.InstanceInterface#getType()
         */
        @Override
        public Type getType()
        {
            return Operator.this.getExpressedType();
        }
        
        /* (non-Javadoc)
         * @see metagrammar.InstanceInterface#getInputTuple()
         */
        public ImmutableList<Operator.Instance> getInputTuple()
        {
            return ImmutableList.copyOf(inputTuple);
        }
        
        /* (non-Javadoc)
         * @see metagrammar.InstanceInterface#getOperator()
         */
        public Operator getOperator()
        {
            return Operator.this;
        }
        
        /* (non-Javadoc)
         * @see metagrammar.InstanceInterface#positionAfter(parsetree.OperatorSpec, metagrammar.Operator.Instance)
         */
        public int positionAfter(OperatorSpec spec, Operator.Instance typed)
        {
            for(int i = spec.getLiteralBeforePosition(); i < inputTypes.length; i++) {
                if(inputTypes[i].equals(typed.getType()) && inputTuple[i].equals(undefinedInstance)) {
                    return i;
                }
            }
            return -1;
        }
        
        /* (non-Javadoc)
         * @see metagrammar.InstanceInterface#positionBefore(parsetree.OperatorSpec, metagrammar.Operator.Instance)
         */
        public int positionBefore(OperatorSpec spec, Operator.Instance typed)
        {
            int end = spec.getLiteralBeforePosition();
            for(int i = 0; i < end; i++) {
                if(inputTypes[i].equals(typed.getType()) && inputTuple[i].equals(undefinedInstance)) {
                    return i;
                }
            }
            return -1;
        }
        
        /* (non-Javadoc)
         * @see metagrammar.InstanceInterface#add(int, metagrammar.Operator.Instance)
         */
        public boolean add(int index, Operator.Instance operatorInstance)
        {
            if(operatorInstance == null) {
                throw new NullPointerException();
            }
            
            if(index < inputTuple.length && inputTuple[index].equals(undefinedInstance)
                    && operatorInstance.getType().isSubtypeOf(inputTypes[index])) {
                inputTuple[index] = operatorInstance;
                return true;
            }
            return false;
        }
        
        /* (non-Javadoc)
         * @see metagrammar.InstanceInterface#set(int, metagrammar.Operator.Instance)
         */
        public Operator.Instance set(int index, Operator.Instance operatorInstance)
        {
            checkNotNull(operatorInstance);
            checkPositionIndex(index, inputTuple.length);
            checkArgument(operatorInstance.getType().isSubtypeOf(inputTypes[index]));
            Operator.Instance oldInput = inputTuple[index];
            inputTuple[index] = operatorInstance;
            return oldInput;
        }
        
        /* (non-Javadoc)
         * @see metagrammar.InstanceInterface#isEquivalentTo(metagrammar.InstanceInterface)
         */
        public boolean isEquivalentTo(Operator.Instance expression)
        {
            return this.canTransformTo(expression) && expression.canTransformTo(this);
        }
        
        /* (non-Javadoc)
         * @see metagrammar.InstanceInterface#canTransformTo(metagrammar.InstanceInterface)
         */
        public boolean canTransformTo(Operator.Instance expression)
        {
            if(this.equals(expression)) {
                return true;
            }
            if(hasSimpleTransformTo(expression)) {
                return true;
            }
            return false;
        }
        
        /**
         * @param expression
         */
        private boolean hasSimpleTransformTo(Operator.Instance expression)
        {
            Set<Transform> simpleTransforms = transformations.get(expression.getOperator());
            for(Transform transform : simpleTransforms) {
                if(transform.isValid(this, expression)) {
                    return true;
                }
            }
            return false;
        }
        
        /* (non-Javadoc)
         * @see metagrammar.InstanceInterface#findTransformTo(metagrammar.InstanceInterface)
         */
        public boolean findTransformTo(Operator.Instance expression)
        {
            HashSet<Operator.Instance> visited = new HashSet<Operator.Instance>();
            visited.add(this);
            return findTransformTo(expression, visited);
        }
        
        public boolean findTransformTo(Operator.Instance expression,
                Set<Operator.Instance> alreadyVisited)
        {
            // TODO
            Set<Transform> simpleTransforms = transformations.get(expression.getOperator());
            for(Transform transform : simpleTransforms) {
                if(transform.isValidInput(this)) {
                    Operator.Instance transformed = transform.transform(this);
                    if(alreadyVisited.add(transformed)) {
                        if(transformed.findTransformTo(expression, alreadyVisited)) {
                            return true;
                        }
                    }
                }
            }
            return false;
        }
        
        public Instance copy()
        {
            Instance root = new Instance();
            for(int i = 0; i < inputTuple.length; i++) {
                root.inputTuple[i] = inputTuple[i];
            }
            return root;
        }
        
        /* (non-Javadoc)
         * @see java.lang.Object#hashCode()
         */
        @Override
        public int hashCode()
        {
            final int prime = 31;
            int result = 1;
            result = prime * result + getOperator().hashCode();
            result = prime * result + Arrays.hashCode(inputTuple);
            return result;
        }
        
        /* (non-Javadoc)
         * @see java.lang.Object#equals(java.lang.Object)
         */
        @Override
        public boolean equals(Object obj)
        {
            if(this == obj) {
                return true;
            }
            if(obj == null) {
                return false;
            }
            if(!(obj instanceof Instance)) {
                return false;
            }
            Instance other = (Instance) obj;
            if(!getOperator().equals(other.getOperator())) {
                return false;
            }
            if(!Arrays.equals(inputTuple, other.inputTuple)) {
                return false;
            }
            return true;
        }
        
        /* (non-Javadoc)
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString()
        {
            return "Instance: " + Arrays.toString(inputTypes) + " → " + expressedType;
        }
        
        /* (non-Javadoc)
         * @see metagrammar.InstanceInterface#verify()
         */
        public boolean verify()
        {
            for(int i = 0; i < inputTuple.length; i++) {
                Operator.Instance input = inputTuple[i];
                Type expectedType = inputTypes[i].getType();
                if(!input.getType().equals(expectedType)) {
                    return false;
                }
                if(!input.verify()) {
                    return false;
                }
            }
            return true;
        }
        
        /* (non-Javadoc)
         * @see metagrammar.InstanceInterface#clearBefore(int)
         */
        public void clearBefore(int tupleIndex)
        {
            for(int i = 0; i < tupleIndex; i++) {
                inputTuple[i] = undefinedInstance;
            }
        }
        
        /* (non-Javadoc)
         * @see metagrammar.InstanceInterface#clearAfter(int)
         */
        public void clearAfter(int tupleIndex)
        {
            checkArgument(tupleIndex >= 0);
            for(int i = tupleIndex; i < inputTuple.length; i++) {
                inputTuple[i] = undefinedInstance;
            }
        }
        
        /* (non-Javadoc)
         * @see metagrammar.InstanceInterface#removeAt(int)
         */
        public Operator.Instance removeAt(int tupleIndex)
        {
            checkPositionIndex(tupleIndex, inputTuple.length);
            Operator.Instance removed = inputTuple[tupleIndex];
            inputTuple[tupleIndex] = undefinedInstance;
            return removed;
        }
        
    }
    
    @XmlID
    @XmlAttribute
    @XmlJavaTypeAdapter(UUIDtoStringAdapter.class)
    private final UUID id;
    
    @XmlJavaTypeAdapter(TypeToUUIDStringAdapter.class)
    private final Type expressedType;
    
    @XmlJavaTypeAdapter(TypeToUUIDStringAdapter.class)
    @XmlList
    private final Type[] inputTypes;
    
    @XmlJavaTypeAdapter(OperatorLinkedHashMultimapAdapter.class)
    private final LinkedHashMultimap<Operator, Transform> transformations;
    
    /**
     * Constructor exclusively for use in deserializing Operators. Its use in other
     * cases will result in an invalid Operator.
     */
    private Operator()
    {
        this.id = null;
        this.expressedType = null;
        this.inputTypes = new Type[] {};
        this.transformations = null;
    }
    
    /**
     * Constructor for an operator that takes no input.
     * 
     * @param expressedType
     */
    public Operator(Type expressedType)
    {
        super();
        id = UUID.randomUUID();
        this.expressedType = expressedType;
        this.inputTypes = new Type[] {};
        this.transformations = LinkedHashMultimap.create();
    }
    
    /**
     * @param expressedType the output type from the function
     * @param inputTypes the types of the input tuple
     */
    public Operator(Type expressedType, Type[] inputTypes)
    {
        super();
        id = UUID.randomUUID();
        this.expressedType = expressedType;
        this.inputTypes = inputTypes;
        this.transformations = LinkedHashMultimap.create();
    }
    
    /* (non-Javadoc)
     * @see metagrammar.Operator#addTransform(metagrammar.TransformSpec)
     */
    public boolean addTransform(Transform transform)
    {
        if(transform.getInputOperator().equals(this)) {
            return transformations.put(transform.getOutputOperator(), transform);
        }
        return false;
    }
    
    /* (non-Javadoc)
     * @see metagrammar.Operator#getInstance(metagrammar.Operator.Instance[])
     */
    public Instance getInstance(Operator.Instance[] inputTuple)
    {
        if(validate(inputTuple)) {
            return new Instance(inputTuple);
        }
        return null;
    }
    
    /* (non-Javadoc)
     * @see metagrammar.Operator#getInstance()
     */
    public Instance getInstance()
    {
        return new Instance();
    }
    
    
    /* (non-Javadoc)
     * @see metagrammar.Operator#validate(metagrammar.Typed[])
     */
    public boolean validate(Typed[] inputTuple)
    {
        // TODO REMOVE
        if(inputTuple.length == inputTypes.length) {
            return validateOrderedTuple(inputTuple);
        }
        return false;
    }
    
    private boolean validateOrderedTuple(Typed[] inputTuple)
    {
        boolean matches = true;
        for(int i = 0; i < inputTypes.length; i++) {
            Typed input = inputTuple[i];
            matches &= (input.getType().equals(inputTypes[i]));
        }
        return matches;
    }
    
    /* (non-Javadoc)
     * @see metagrammar.Operator#takesInput()
     */
    public boolean takesInput()
    {
        return inputTypes != null && inputTypes.length > 0;
    }
    
    /* (non-Javadoc)
     * @see metagrammar.Operator#getInputTypes()
     */
    public ImmutableList<Type> getInputTypes()
    {
        return ImmutableList.copyOf(inputTypes);
    }
    
    /* (non-Javadoc)
     * @see metagrammar.Operator#getExpressedType()
     */
    public Type getExpressedType()
    {
        return expressedType;
    }
    
    /* (non-Javadoc)
     * @see metagrammar.Operator#getType()
     */
    @Override
    public Type getType()
    {
        return expressedType;
    }
    
    /* (non-Javadoc)
     * @see metagrammar.Operator#getTransformations()
     */
    public ImmutableMultimap<Operator, Transform> getTransformations()
    {
        return ImmutableMultimap.copyOf(transformations);
    }
    
    /* (non-Javadoc)
     * @see metagrammar.Operator#hashCode()
     */
    @Override
    public int hashCode()
    {
        return id.hashCode();
    }
    
    /* (non-Javadoc)
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object obj)
    {
        if(this == obj) {
            return true;
        }
        if( !(obj instanceof Operator)) {
            return false;
        }
        Operator other = (Operator) obj;
        if(!id.equals(other.id)) {
            return false;
        }
        return true;
    }
    
    /* (non-Javadoc)
     * @see metagrammar.Operator#toString()
     */
    @Override
    public String toString()
    {
        return "Operator: " + Arrays.toString(inputTypes) + " → " + expressedType;
    }
    
    /* (non-Javadoc)
     * @see metagrammar.Operator#getId()
     */
    @Override
    public UUID getId()
    {
        return id;
    }
    
}
